using System;
using System.Diagnostics;
using System.Threading;
using System.Net;
using System.Net.Sockets;

public class Connection : IDisposable
{
    private readonly Session m_session;
    private int m_active;

    private Socket m_socket;
    private string m_host;
    private int m_port;

    private IPAddress[] m_addresses;
    private int m_tryindex;

    CircularBuffer m_inBuffer;
    SendDoubleBuffer m_outBuffer;

    private int m_writeFlag;
    private SocketAsyncEventArgs m_writeArgs;
    private SocketAsyncEventArgs m_readArgs;
    private SocketAsyncEventArgs m_connectArgs;

    public Connection(Session session)
    {
        m_session = session;

        m_inBuffer = new CircularBuffer(65536);
        m_outBuffer = new SendDoubleBuffer();

        m_connectArgs = new SocketAsyncEventArgs();
        m_connectArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnConnectCompleted);
        m_readArgs = new SocketAsyncEventArgs();
        m_readArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnReceiveCompleted);
        m_writeArgs = new SocketAsyncEventArgs();
        m_writeArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
    }

    public void Start(string host, int port)
    {
        m_host = host;
        m_port = port;
        Interlocked.Exchange(ref m_active, 1);
        ThreadPool.QueueUserWorkItem(s => StartConnect());
    }

    public void Close()
    {
        if (Interlocked.CompareExchange(ref m_active, 0, 1) == 1)
        {
            m_socket.Dispose();
            m_session.OnDisconnected();
        }
    }

    public void TryWrite()
    {
        if (Interlocked.CompareExchange(ref m_writeFlag, 1, 0) == 0)
        {
            ThreadPool.QueueUserWorkItem(s => StartSendAsync());
        }
    }

    public SendDoubleBuffer GetOutBuffer()
    {
        return m_outBuffer;
    }

    public bool IsActive()
    {
        return m_active != 0;
    }

    private void StartConnect()
    {
        m_addresses = Dns.GetHostAddresses(m_host);
        m_tryindex = 0;
        TryConnectAsync();
    }

    private void TryConnectAsync()
    {
        if (!IsActive())
        {
            return;
        }
        for (; m_tryindex < m_addresses.Length; ++m_tryindex)
        {
            var address = m_addresses[m_tryindex];
            switch (address.AddressFamily)
            {
                case AddressFamily.InterNetwork:
                case AddressFamily.InterNetworkV6:
                    m_connectArgs.RemoteEndPoint = new IPEndPoint(address, m_port);
                    m_socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                    if (!m_socket.ConnectAsync(m_connectArgs))
                    {
                        OnConnectCompleted(m_socket, m_connectArgs);
                    }
                    return;
            }
        }
        Close();
    }

    private void OnConnectCompleted(object sender, SocketAsyncEventArgs e)
    {
        if (!IsActive())
        {
            return;
        }
        if (e.SocketError != SocketError.Success)
        {
            m_tryindex += 1;
            m_socket.Dispose();
            TryConnectAsync();
            return;
        }
        if (m_session != null)
        {
            m_session.OnConnected();
        }
        StartReceiveAsync();
    }

    private void StartReceiveAsync()
    {
        if (!IsActive())
        {
            return;
        }
        m_readArgs.SetBuffer(m_inBuffer.GetBuffer(),
            (int)m_inBuffer.GetContiguiousWritableOffset(),
            (int)m_inBuffer.GetContiguiousWritableSpace());
        if (!m_socket.ReceiveAsync(m_readArgs))
        {
            OnReceiveCompleted(m_socket, m_readArgs);
        }
    }

    private void OnReceiveCompleted(object sender, SocketAsyncEventArgs e)
    {
        if (!IsActive())
        {
            return;
        }
        if (e.SocketError != SocketError.Success)
        {
            Close();
            return;
        }
        var buffer = m_inBuffer.GetBuffer();
        var offset = (int)m_inBuffer.GetContiguiousWritableOffset();
        var count = (int)m_inBuffer.GetContiguiousWritableSpace();
        Debug.Assert(e.Buffer == buffer && e.Offset == offset && e.Count <= count);
        m_inBuffer.IncrementContiguiousWritten((ulong)e.BytesTransferred);
        while (true)
        {
            var packet = ReadPacketFromBuffer();
            if (packet != null)
            {
                m_session.PushRecvPacket(packet);
            }
            else
            {
                break;
            }
        }
        StartReceiveAsync();
    }

    private void StartSendAsync()
    {
mark:   if (!IsActive())
        {
            return;
        }
        var data = m_outBuffer.GetSendData(out int offset, out int count);
        if (data != null && count > 0)
        {
            m_writeArgs.SetBuffer(data, offset, count);
            if (!m_socket.SendAsync(m_writeArgs))
            {
                OnSendCompleted(m_socket, m_writeArgs);
            }
        }
        else
        {
            Interlocked.Exchange(ref m_writeFlag, 0);
            if (m_outBuffer.GetSendDataSize() > 0 &&
                Interlocked.CompareExchange(ref m_writeFlag, 1, 0) == 0)
            {
                goto mark;
            }
        }
    }

    private void OnSendCompleted(object sender, SocketAsyncEventArgs e)
    {
        if (!IsActive())
        {
            return;
        }
        if (e.SocketError != SocketError.Success)
        {
            Close();
            return;
        }
        var data = m_outBuffer.GetSendData(out int offset, out int count);
        Debug.Assert(e.Buffer == data && e.Offset == offset && e.Count <= count);
        m_outBuffer.RemoveSendData(e.BytesTransferred);
        StartSendAsync();
    }

    private NetPacket ReadPacketFromBuffer()
    {
        if (!IsActive())
        {
            return null;
        }
        var dataLen = m_inBuffer.GetReadableSpace();
        if (dataLen < NetPacket.HEADER_SIZE)
        {
            return null;
        }
        var header = new NetPacket(NetPacket.HEADER_SIZE);
        header.Erlarge(NetPacket.HEADER_SIZE);
        m_inBuffer.Peek(header.GetBuffer(), 0, header.GetSize());
        header.ReadPacketHeader(out ushort opcode, out ushort size);
        if (dataLen < size)
        {
            return null;
        }
        var packet = new NetPacket(opcode, size - NetPacket.HEADER_SIZE);
        packet.Erlarge(size - NetPacket.HEADER_SIZE);
        m_inBuffer.Remove(NetPacket.HEADER_SIZE);
        m_inBuffer.Read(packet.GetBuffer(), 0, packet.GetSize());
        return packet;
    }

    #region IDisposable Support
    private bool disposedValue = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                Interlocked.Exchange(ref m_active, 0);
            }
            if (m_socket != null)
            {
                m_socket.Dispose();
            }
            m_writeArgs.Dispose();
            m_readArgs.Dispose();
            m_connectArgs.Dispose();
            m_inBuffer = null;
            m_outBuffer = null;
            disposedValue = true;
        }
    }

    ~Connection()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    #endregion
}
